using System.Text;
using System.Web;

namespace JTLeigh.Tools.Development.CopySourceAsHtml
{

    /// <summary>
    /// Represents a block of content in an HTML line.
    /// </summary>
    public sealed class HtmlBlock
    {
        #region Static Methods

        /// <summary>
        /// Gets a block of whitespace made up of breaking and non-breaking spaces.
        /// </summary>
        /// <param name="allowLeadingSpace"><see langword="true"/> if the leading whitespace 
        /// character can be a breaking space, <see langword="false"/> otherwise.</param>
        /// <param name="spaces">The number of characters of whitespace to get.</param>
        /// <param name="tabSize">The tab size.</param>
        /// <returns>The block of whitespace.</returns>
        /// <remarks>
        /// <para>
        /// The exact pattern of breaking and non-breaking spaces returned depends on the number
        /// of spaces, the tab size, and whether the leading whitespace character can be a breaking
        /// space or not.  The unit tests for this class 
        /// </para>
        /// </remarks>
        internal static string GetWhiteSpace(bool allowLeadingSpace, int spaces, int tabSize)
        {
            StringBuilder whiteSpace;
            int space;

            Safety.AssertIsTrue((spaces >= 0), "spaces is less than 0.");
            Safety.AssertIsTrue((tabSize >= 1), "tabSize is less than 1.");

            switch (spaces)
            {
                case 0:
                    return "";
                case 1:
                    if (allowLeadingSpace)
                    {
                        return " ";
                    }
                    else
                    {
                        return "&nbsp;";
                    }
                default:

                    // NOTE: If we continued with a tab size of 1, we would get any number of 
                    // breaking spaces in a row, with only the second-last space being a 
                    // non-breaking space, which wouldn't render render correctly.  Instead, we 
                    // special-case tab size 1 so that it's treated as if it were tab size 2.
                    if (tabSize == 1)
                    {
                        tabSize = 2;
                    }

                    whiteSpace = new StringBuilder(spaces);
                    for (space = 1; space < spaces; space++)
                    {
                        if ((space % tabSize) == 0)
                        {
                            whiteSpace.Append(' ');
                        }
                        else
                        {
                            whiteSpace.Append("&nbsp;");
                        }
                    }
                    // NOTE: We need to append the final breaking space.  If the last character was
                    // a breaking space, we need to transform it to a non-breaking space, otherwise
                    // we'll wind up with two breaking spaces in a row (one of which may be ignored
                    // when the HTML is rendered).
                    if (whiteSpace[whiteSpace.Length - 1] == ' ')
                    {
                        // NOTE: We can transform and append at the same time simply by inserting 
                        // a non-breaking space before the final breaking space.
                        whiteSpace.Insert(whiteSpace.Length - 1, "&nbsp;");
                    }
                    else
                    {
                        whiteSpace.Append(' ');
                    }
                    return whiteSpace.ToString();
            }
        }

        #endregion // Static Methods

        #region Fields

        private StringBuilder _content;

        #endregion // Fields

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="HtmlBlock"/> class.
        /// </summary>
        public HtmlBlock()
        {
            _content = new StringBuilder();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="HtmlBlock"/> class with the specified 
        /// parent block.
        /// </summary>
        /// <param name="parentBlock">The parent block.</param>
        /// <remarks>
        /// <para>
        /// The newly initialized block will inherit the parent block's foreground color, 
        /// background color, and bold state.
        /// </para>
        /// </remarks>
        public HtmlBlock(HtmlBlock parentBlock)
            : this()
        {
            Safety.AssertArgumentNotNull("parentBlock", parentBlock);

            ForegroundColor = parentBlock.ForegroundColor;
            BackgroundColor = parentBlock.BackgroundColor;
            Bold = parentBlock.Bold;
        }

        #endregion // Constructors

        #region Properties

        /// <summary>
        /// Gets or sets the foreground color of the block.
        /// </summary>
        /// <value>
        /// The foreground color of the block.
        /// </value>
        public int ForegroundColor { get; set; }

        /// <summary>
        /// Gets or sets the background color of the block.
        /// </summary>
        /// <value>
        /// The background color of the block.
        /// </value>
        public int BackgroundColor { get; set; }

        /// <summary>
        /// Gets or sets the bold state of the block.
        /// </summary>
        /// <value>
        /// <see langword="true"/> if the block is bold, <see langword="false"/> otherwise.
        /// </value>
        public bool Bold { get; set; }

        /// <summary>
        /// Gets whether the block has any content.
        /// </summary>
        /// <value>
        /// <see langword="true"/> if the block has any content, <see langword="false"/> otherwise.
        /// </value>
        public bool HasContent
        {
            get { return (_content.Length > 0); }
        }

        /// <summary>
        /// Gets the number of leading spaces for the block.
        /// </summary>
        /// <value>
        /// The number of leading spaces for the block.
        /// </value>
        public int LeadingSpaces { get; private set; }

        /// <summary>
        /// Gets the number of trailing spaces for the block.
        /// </summary>
        /// <value>
        /// The number of trailing spaces for the block.
        /// </value>
        internal int TrailingSpaces { get; private set; }

        #endregion // Properties

        #region Methods

        /// <summary>
        /// Appends a character to the block.
        /// </summary>
        /// <param name="value">The character.</param>
        /// <param name="tabSize">The tab size.</param>
        /// <remarks>
        /// <para>
        /// This class treats spaces differently than other characters.  Instead of appending 
        /// whitespace to the content immediately, it the number of leading and trailing spaces, 
        /// and only gets the whitespace when it knows exactly how many consecutive spaces there
        /// are.  This allows the class to take tab sizes into account, and generate whitespace 
        /// with breaking and non-breaking spaces as needed.
        /// </para>
        /// <para>
        /// The <paramref name="tabSize"/> parameter is used to get the necessary whitespace when 
        /// appending a non-whitespace character after trailing spaces.
        /// </para>
        /// </remarks>
        public void Append(char value, int tabSize)
        {
            Safety.AssertIsTrue((tabSize >= 1), "tabSize is less than 1.");

            if (value == ' ')
            {
                if (!HasContent)
                {
                    // NOTE: We don't append leading spaces to the content.  Instead, we track the number 
                    // of leading spaces, and use it to get the necessary whitespace when we convert the 
                    // block to HTML.
                    LeadingSpaces++;
                }
                else
                {
                    // NOTE: We don't append trailing spaces to the content immediately.  Instead,
                    // we track the number of leading spaces, and use it to get the necessary 
                    // whitespace when we either append a non-whitespace character or we convert 
                    // the block to HTML.
                    TrailingSpaces++;
                }
            }
            else
            {
                // NOTE: If there are any trailing spaces, we need to append those first.
                if (TrailingSpaces > 0)
                {
                    _content.Append(GetWhiteSpace(true, TrailingSpaces, tabSize));
                    TrailingSpaces = 0;
                }
                // NOTE: Pass the value through Tools.HtmlEncode before appending it to the string.
                _content.Append(HtmlTools.HtmlEncode(value));
            }
        }

        /// <summary>
        /// Gets the contents of the block with leading and trailing spaces transformed into any 
        /// necessary whitespace.
        /// </summary>
        /// <param name="allowLeadingSpace"><see langword="true"/> to allow leading spaces, 
        /// <see langword="false"/> to disallow leading spaces.</param>
        /// <param name="outdentSpaces">The number of spaces to reduce the leading spaces by.</param>
        /// <param name="tabSize">The tab size.</param>
        /// <returns>The contents of the block with </returns>
        public string GetHtml(bool allowLeadingSpace, int outdentSpaces, int tabSize)
        {
            StringBuilder html;

            Safety.AssertIsTrue((outdentSpaces >= 0), "outdentSpaces is less than 0.");
            Safety.AssertIsTrue((tabSize >= 1), "tabSize is less than 1.");
            Safety.AssertIsTrue((outdentSpaces <= LeadingSpaces), "outdentSpaces is greater than LeadingSpaces.");

            html = new StringBuilder();
            html.Append(GetWhiteSpace(allowLeadingSpace, (LeadingSpaces - outdentSpaces), tabSize));
            html.Append(_content.ToString());
            html.Append(GetWhiteSpace(true, TrailingSpaces, tabSize));
            return html.ToString();
        }

        #endregion // Methods
    }

}
